#!/usr/bin/env node

const os= require('os');
const mongojs = require('mongojs');
const fs = require('fs-extra');
const path = require('path');
const moment = require('moment');
const command = require('./js/command');
const utils = require('./js/utils');
const _ = require('lodash');
const program = require('commander');
const shell = require('shelljs');
const configFile = path.join(os.homedir(), '.mongo_config');
const CACHE = {};
let _database = 'admin';
let _host = 'localhost';

let _verbose = true;
let _rawMode = false;
let _prettyShell = true;
let _highLightStyle;
let _limit = 1;
let _maxLimit = 1000;
async function createMongo (commands, options) {
     const db = mongojs(`mongodb://${_host}:27017/${_database}`);
    const tables = await getTables(db);
    for (const table of tables) {
        await getFieldsOfTable(db, table);
    }

    // 添加表，字段的自动完成
    function completer(line) {
        const words = line.replace(/\s,\s/g, ',').trimLeft().split(/\s/);
        const linePartial = _.last(_.last(words).split(','));
        let list = [];
        if (words.length <= 1) {
            const _linePartial = line.trim();
            const _tables = CACHE._tables.map(o=>`db.${o}`)
            list = _tables.filter((o) => o.startsWith(_linePartial));
            const found = list.filter((o) => o.startsWith(_linePartial));
            return [found.length ? found : list, linePartial];
        }

        const firstWord = _.toLower(words[0]);
        const preWord = _.toLower(_.nth(words, -2));
        if (firstWord === 'd') {
            list = CACHE._tables.filter((o) => o.startsWith(linePartial));
        } else if (_.includes(['ls', 'cd', 's', 'm', 'r', 'c', 'dump'], preWord)) {
            list = CACHE._tables.filter((o) => o.startsWith(linePartial));
        }  else {
            const prePreWord = _.toLower(_.nth(words, -3));
            if (_.includes(['s', 'm', 'r', 'c'], prePreWord)) {
                const table = preWord;
                const fields = CACHE[table];
                list = fields.filter((o) => o.startsWith(linePartial));
            } else {
                const prePrePreWord = _.toLower(_.nth(words, -4));
                if (_.includes(['s', 'm', 'c'], prePrePreWord)) {
                    const table = prePreWord;
                    const fields = CACHE[table];
                    list = fields.filter((o) => o.startsWith(linePartial));
                } else {
                    let allFileds = [];
                    for (const table of CACHE._tables) {
                        const fields = CACHE[table];
                        allFileds = [...allFileds, ...fields];
                    }
                    allFileds = _.uniq(allFileds);
                    list = allFileds.filter((o) => o.startsWith(linePartial));
                }
            }
        }
        const found = list.filter((o) => o.startsWith(linePartial));
        return [found.length ? found : list, linePartial];
    }
    CACHE._options = { ...options, db, completer };
    command(commands, CACHE._options);
}
function dropBrack(line) {
    while (_.first(line) === '(' && _.last(line) === ')') {
        line = line.substr(1, line.length-2);
    }
    return line;
}

function parseItem(item) {
    const list = item.match(/([\w\.]+)(!=|>=|>|<=|<|=|~|#)(.*)/);
    if (!list) {
        return null;
    }
    let key = list[1];
    let oper = list[2];
    let value = list[3];

    if (key === undefined || oper === undefined || value === undefined) {
        return null;
    }
    if (value.length <16 && +value == value) {
        value = +value;
    } else if (value === 'null') {
        value = null;
    } else if (value === 'true') {
        value = true;
    } else if (value === 'false') {
        value = false;
    } else if (value === '$exists') {
        if (oper === '=') {
            return { [key]: { $exists: true } };
        } else {
            return { [key]: { $exists: false } };
        }
    } else if (/^[a-z0-9]{24}$/.test(value)) {
        value = mongojs.ObjectId(value);
    } else if (/^'\d{4}-\d{2}-\d{2}'$/.test(value)) {
        value = moment(value, 'YYYY-MM-DD').toDate();
    } else if (/^'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}'$/.test(value)) {
        value = moment(value, 'YYYY-MM-DD HH:mm:ss').toDate();
    } else if (/^'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}'$/.test(value)) {
        value = moment(value, 'YYYY-MM-DD HH:mm:ss.SSS').toDate();
    } else if (/^'.*'$|^".*"$/.test(value)) {
        value = value.substr(1, value.length-2);
    }
    if (_.endsWith(key, '.length')) {
        return { $where: `this.${key}${oper === '=' ? '==' : oper}${value}` };
    }
    switch (oper) {
        case '!=':return { [key]: { $ne: value } };
        case '>': return { [key]: { $gt: value } };
        case '>=': return { [key]: { $gte: value } };
        case '=': return { [key]: value };
        case '<': return { [key]: { $lt: value } };
        case '<=': return { [key]: { $lte: value } };
        case '~': {
            value = ''+value;
            return { [key]: new RegExp(value.replace(/</, '^').replace(/>/, '$')) };
        }
        case '#': {
            const list = value.split(',');
            const _list = [];
            for (const item of list) {
                if (/^'.*'$|^".*"$/.test(item)) {
                    _list.push(item.replace(/^['"](.*)['"]$/, '$1'));
                } else if (+item == item) {
                    _list.push(+item);
                } else {
                    _list.push(item);
                }
            }
            return { [key]: { $in: _list } };
        }
    }
    return null;
}
function parseAnd(line) {
    if (!/\||&/.test(line)) {
        return parseItem(line);
    }
    const list = [];
    let i = -1, max = line.length - 1, item = '', match = 0;
    while (i++ < max) {
        const ch = line[i];
        if (ch === '(') {
            match++;
        } else if (ch === ')') {
            match--;
        }
        if (ch === '&' && match === 0) {
            list.push(parseAnd(dropBrack(item)));
            item='';
        } else {
            item=`${item}${ch}`;
        }
    }
    if (match === 0) { // 如果括号不匹配，则丢弃
        if (line === item) {
            item && list.push(parseOr(dropBrack(item)));
        } else {
            item && list.push(parseAnd(dropBrack(item)));
        }
    }
    return list.length > 1 ? { $and: list } : list[0];
}
function parseOr(line) {
    if (!/\||&/.test(line)) {
        return parseItem(line);
    }
    const list = [];
    let i = -1, max = line.length - 1, item = '', match = 0;
    while (i++ < max) {
        const ch = line[i];
        if (ch === '(') {
            match++;
        } else if (ch === ')') {
            match--;
        }
        if (ch === '|' && match === 0) {
            list.push(parseOr(dropBrack(item)));
            item='';
        } else {
            item=`${item}${ch}`;
        }
    }
    if (match === 0) { // 如果括号不匹配，则丢弃
        if (line === item) {
            item && list.push(parseAnd(dropBrack(item)));
        } else {
            item && list.push(parseOr(dropBrack(item)));
        }
    }
    return list.length > 1 ? { $or: list } : list[0];
}
function formatRaw(obj) {
    if (!obj || !_.isObject(obj)) {
        return obj + `[${typeof obj}]`;
    }
    if (_.isArray(obj)) {
        return obj.map(o=>formatRaw(o));
    }
    for (const key in obj) {
        const item = obj[key];
        if (item instanceof Array) {
            obj[key] = formatRaw(item);
        } else if (item instanceof Date) {
            obj[key] = item + '[date]';
        } else if (/^[a-z0-9]{24}$/.test(item+'')) {
            obj[key] = item + `[ObjectId]`;
        } else {
            obj[key] = item + `[${typeof item}]`;
        }
    }
    return obj;
}
function formatResult(obj) {
    if (!obj || !_.isObject(obj)) {
        return obj;
    }
    if (_.isArray(obj)) {
        return obj.map(o=>formatResult(o));
    }
    for (const key in obj) {
        const item = obj[key];
        if (item instanceof Array) {
            obj[key] = formatResult(item);
        } else if (item instanceof Date) {
            obj[key] = moment(item).format('YYYY-MM-DD HH:mm:ss.SSS');
        }
    }
    if (obj._id) {
        obj.id = obj._id;
        delete obj._id;
    }
    return obj;
}
function formatCond(line) {
    if (!line) {
        return {};
    }
    if (/^[0-9a-z]{24}$/.test(line)) {
        return { _id: mongojs.ObjectId(line) };
    }
    if (/^1[0-9]{10}$/.test(line)) {
        return { phone: line };
    }
    if (/^[0-9]{17}[0-9xX]$/.test(line)) {
        return { idNo: line };
    }
    return parseOr(dropBrack(line));
}
function formatSetFields(line) {
    const list = [];
    let flag = true;
    while (flag) {
        let matches = line.match(/^([a-zA-Z][a-zA-Z0-9_]*):(.*)/);
        if (matches) {
            const key = matches[1];
            let value = matches[2];
            if (/^[^;]*=>{/.test(value)) { //带{函数
                let i = -1, max = value.length, match = 0;
                while (i++ < max) {
                    const ch = value[i];
                    if (ch === '{') {
                        match++;
                    } else if (ch === '}') {
                        match--;
                    }
                    if ((ch === ';' || !ch) && match === 0) {
                        line = value.substr(i+1);
                        value = value.substr(0, i);
                        list.push({ key, value });
                        if (!line) {
                            flag = false;
                        }
                        break;
                    }
                }
                if (match !== 0) {
                    return null;
                }
            } else if (_.startsWith(value, '[')) { // 数组
                let i = -1, max = value.length, bmatch = 0, cmatch = 0;
                while (i++ < max) {
                    const ch = value[i];
                    if (ch === '[') {
                        bmatch++;
                    } else if (ch === ']') {
                        bmatch--;
                    } else if (ch === '{') {
                        cmatch++;
                    } else if (ch === '}') {
                        cmatch--;
                    }
                    if ((ch === ';' || !ch) && bmatch === 0) {
                        if (cmatch !== 0) {
                            return null;
                        }
                        line = value.substr(i+1);
                        value = value.substr(0, i);
                        list.push({ key, value });
                        if (!line) {
                            flag = false;
                        }
                        break;
                    }
                }
                if (bmatch !== 0) {
                    return null;
                }
            } else if (_.startsWith(value, '{')) { // 对象
                let i = -1, max = value.length, bmatch = 0, cmatch = 0;
                while (i++ < max) {
                    const ch = value[i];
                    if (ch === '[') {
                        bmatch++;
                    } else if (ch === ']') {
                        bmatch--;
                    } else if (ch === '{') {
                        cmatch++;
                    } else if (ch === '}') {
                        cmatch--;
                    }
                    if ((ch === ';' || !ch) && cmatch === 0) {
                        if (bmatch !== 0) {
                            return null;
                        }
                        line = value.substr(i+1);
                        value = value.substr(0, i);
                        list.push({ key, value });
                        if (!line) {
                            flag = false;
                        }
                        break;
                    }
                }
                if (cmatch !== 0) {
                    return null;
                }
            } else {
                matches = value.match(/([^;]*);(.*)/);
                if (!matches) {
                    list.push({ key, value: value === '' ? undefined :  value === `''` || value === `""` ? '' : value});
                    break;
                }
                value = matches[1];
                list.push({ key, value: value === '' ? undefined :  value === `''` || value === `""` ? '' : value});
                line = matches[2];
            }
        } else {
            return null;
        }
    }
    return list;
}
function formatUpdate(self, doc, setFields) {
    const set = {};
    const unset = {};
    for (const item of setFields) {
        const key = item.key;
        let value = item.value;

        if (value === 'true') {
            set[key] = true;
            continue;
        }
        if (value === 'false') {
            set[key] = false;
            continue;
        }
        if (value === 'null') {
            set[key] = null;
            continue;
        }
        if (value === 'undefined' || value === undefined || value === '$unset') {
            unset[key] = 1;
            continue;
        }
        // 解析函数
        value = utils.parseFunction(value, true);
        if (_.isFunction(value)) {
             set[key] = value(doc[key], {_, utils, moment, $$: doc});
            continue;
        }
        // 解析数组
        if (/^\[.*]$/.test(value)) {
            try {
                set[key] = (new Function(`return ${value}`))();
            } catch(e) {
                self.error(`Array:${value} 格式错误`);
                return null;
            }
            continue;
        }
        // 解析obj
        if (/^{.*}$/.test(value)) {
            try {
                set[key] = (new Function(`return ${value}`))();
            } catch(e) {
                self.error(`Object:${value} 格式错误`);
                return null;
            }
            continue;
        }
        if (/^[0-9a-z]{24}$/.test(value)) {
            set[key] = mongojs.ObjectId(value);
            continue;
        }
        if (+value == value) {
            value = +value
        } else if (/\+|-|\*|\//.test(value)) {
            try {
                let _val = eval(value);
                if (typeof _val === 'number') {
                    value = _val;
                } else {
                    _val = value.replace(/^'(.*)'$/, '$1');
                    // 解析时间
                    if (/^\d{4}-\d{2}-\d{2}$/.test(_val)) {
                        set[key] = moment(_val, 'YYYY-MM-DD').toDate();
                        continue;
                    }
                    if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(_val)) {
                        set[key] = moment(_val, 'YYYY-MM-DD HH:mm:ss').toDate();
                        continue;
                    }
                    if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}$/.test(_val)) {
                        set[key] = moment(_val, 'YYYY-MM-DD HH:mm:ss.SSS').toDate();
                        continue;
                    }
                }
            } catch(err) {}
        }
        if (/^'.*'$|^".*"$/.test(value)) {
            value = value.replace(/^['"](.*)['"]$/, '$1');
        }
        set[key] = value;
    }
    return { ...(_.size(set) ? { $set: set } : {}), ...(_.size(unset) ? { $unset: unset } : {}) };
}
async function formatFields(db, table, line, cond) {
    if (!line || line === '*') {
        return {};
    }
    let include = true;
    if (line[0] === '-') {
        include = false;
        line = line.substr(1);
    }
    let list = line.split(',').filter(Boolean);
    const allFields = await getFieldsOfTable(db, table, true, cond);
    list = list.map(o => new RegExp(o.replace('<', '^').replace('>', '$'), 'i'));

    const fields = {};
    allFields.filter(o=>_.some(list, r=>r.test(o))).forEach(o=>fields[o] = include ? 1 : 0);
    return fields;
}
async function formatSort(db, table, line) {
    if (!line) {
        return {};
    }
    const sort = {};
    const allFields = await getFieldsOfTable(db, table);
    line = line.split(',');
    for (const i in line) {
        let item = line[i];
        let order = item[item.length-1] === '+' ? 1 : -1;
        item = item.substr(0, item.length-1);
        if (_.includes(allFields, item)) {
            sort[item] = order;
        }
    }
    return sort;
}
function formatLine(self, line) {
    let i = -1, max = line.length - 1, newLine = '', amatch = 0, bmatch = 0, cmatch = 0;
    while (i++ < max) {
        let ch = line[i];
        if (ch === '[') {
            amatch++;
        } else if (ch === ']') {
            amatch--;
        }
        if (ch === '{') {
            bmatch++;
        } else if (ch === '}') {
            bmatch--;
        }
        if (ch === '(') {
            cmatch++;
        } else if (ch === ')') {
            cmatch--;
        }
        if (amatch < 0 || bmatch < 0 || cmatch < 0) {
            self.error('括号不匹配');
            return [];
        }
        if (!(ch === ' ' && (amatch || bmatch || cmatch))) {
            newLine += ch;
        }
    }
    if (amatch || bmatch || cmatch) {
        self.error('括号不匹配');
        return [];
    }
    return newLine
    .replace(/"[^"]+"/g, o=>o.replace(' ', '&nbsp;'))
    .replace(/'[^']+'/g, o=>o.replace(' ', '&nbsp;'))
    .replace(/\s*:\s*/g, ':')
    .replace(/\s*;\s*/g, ';')
    .replace(/\s*,\s*/g, ',')
    .replace(/\s*!=\s*/g, '!=')
    .replace(/\s*!==\s*/g, '!==')
    .replace(/\s*=\s*/g, '=')
    .replace(/\s*==\s*/g, '==')
    .replace(/\s*~\s*/g, '~')
    .replace(/\s*\|\s*/g, '|')
    .replace(/\s*&\s*/g, '&')
    .replace(/\s*#\s*/g, '#')
    .trim().split(/\s/).filter(Boolean).map(o=>o.replace('&nbsp;', ' '));
}
function j2s(obj) {
    return JSON.stringify(obj, (k, v)=>{
        if (v instanceof RegExp) {
            return v.toString();
        }
        return v;
    });
}
function getCollectionNames(db) {
    return new Promise(resolve=>{
        db.getCollectionNames((err, collections)=>{
            if (err) {
                resolve([]);
            } else {
                resolve(collections);
            }
        });
    });
}
function getFields(db, table, cond) {
    return new Promise(resolve=>{
        db[table].find(cond||{}).limit(100).toArray((err, docs)=>{
            if (err) {
                resolve([]);
            } else {
                resolve(_.keys(_.reduce(docs, (r, o)=>({...r, ...o}), {})));
            }
        });
    });
}
async function getTables(db, force) {
    if (!CACHE._tables || force) {
        CACHE._tables = await getCollectionNames(db);
    }
    return CACHE._tables;
}
async function getFieldsOfTable(db, table, force, cond) {
    if (!CACHE[table] || force) {
        CACHE[table] = await getFields(db, table, cond);
    }
    return CACHE[table];
}
async function nativeMongo(self, line, isGlobal) {
    const MOCMD = isGlobal ? `mongo ${_host}:27017 --eval` : `mongo ${_host}:27017/${_database} --eval`;
    return new Promise(resolve=>{
        if (!line) {
            resolve();
        }
        if (self && _verbose) {
            self.verbose(`${MOCMD} "${line}"`)
        }
        line = line.replace(/\$/g, '\\$');
        shell.exec(`${MOCMD} "${line}"`, { silent:false }, (code, stdout, stderr) => {
            if (stderr) {
                resolve();
            } else if (stdout) {
                const list = stdout.split('\n').filter(o=>o && !/MongoDB|connecting|Implicit/.test(o));
                if (!list.length) {
                    return resolve();
                }
                const _list = [];
                for (let item of list) {
                    item = item.replace(/ObjectId\(([^)]*)\)/g, '$1');
                    item = item.replace(/NumberLong\(([^)]*)\)/g, '$1');
                    item = item.replace(/ISODate\(([^)]*)\)/g, '$1');
                    try {
                        _list.push(JSON.parse(item));
                    } catch (e) {
                        _list.push(item);
                    }
                }
                resolve(_list);
            }
        });
    });
}
async function executeLine(line, { db }) {
    if (!/^db\./.test(line)) {
        return this.prompt();
    }
    line = line.replace(/"/g, `'`);
    const ret = await nativeMongo(this, line);
    if (!ret) {
        return this.prompt();
    }
    showJson(this, ret);
}
function showJson(self, docs) {
    if (_rawMode) {
        self.showJson(formatRaw(docs), _prettyShell, _highLightStyle);
    } else {
        self.showJson(formatResult(docs), _prettyShell, _highLightStyle);
    }
}
function getDbs(db) {
    return new Promise(resolve=>{
        db._getConnection(async (err, conn)=>{
            if (err) return resolve([]);
            const result = await conn.db('admin').command({ listDatabases: 1, nameOnly: true });
            resolve(result.databases.map(o => o.name));
        });
    });
}
async function showTables(line, { db }) {
    if (!line || line === '.') { // show tables
        this.print('tables in '+ _database +' :', 'red');
        const tables = await getTables(db, true);
        this.print(tables.join('  '), 'green');
        this.prompt();
    } else if (line === '..' || line === '-') { // show databases
        this.print('databases :', 'red');
        const dbs = await getDbs(db);
        this.print(dbs.join(' '), 'green');
        this.prompt();
    } else { // show table fields
        line = line.replace(/[^\w]*/g, '');
        const fields = await getFieldsOfTable(db, line, true);
        this.print(fields.join('  '), 'green');
        this.prompt();
    }
}
function getLimitIndex(list) {
    return _.findIndex(list, o=>+o==o && o<=_maxLimit);
}
function getCondIndex(list) {
    return _.findIndex(list, o=>/['"\w](!=|>=|(?<!=)>|=(?!=)|<=|<|~|#)[\w'"]/.test(o) || /^[0-9a-z]{24}$/.test(o) || /^1[0-9]{10}$/.test(o) || /^[0-9]{17}[0-9xX]$/.test(o));
}
function getSortIndex(list) {
    return _.findIndex(list, o=>/[a-zA-Z][a-zA-Z0-9_]*(\+|-)$/.test(o));
}
function getSetFieldsIndex(list) {
    return _.findIndex(list, o=>/^[a-zA-Z][a-zA-Z0-9_]*:/.test(o));
}
async function showTableRows (line, { db }) {
    if (!line) {
        return this.prompt();
    }
    const matches = line.match(/out=[^\s]*/);
    let writeFile;
    if (matches) {
        writeFile = matches[0].replace('out=', '');
        line = line.replace(matches[0], '');
    }

    let showCount = false;
    let list = formatLine(this, line);
    const table = list[0];
    if (!table) {
        return this.error('invalid oper');
    }
    list = _.drop(list);

    let limit = 0;
    let limitIndex = getLimitIndex(list);
    if (limitIndex !== -1) {
        limit = +list[limitIndex];
        list.splice(limitIndex, 1);
        if (limit < 0) {
            showCount = true;
        }
    } else {
        limit = _limit;
    }

    let sort = '';
    let sortIndex = getSortIndex(list);
    if (sortIndex !== -1) {
        sort = await formatSort(db, table, list[sortIndex]);
        list.splice(sortIndex, 1);
    }

    let setFields;
    let setFieldsIndex = getSetFieldsIndex(list);
    if (setFieldsIndex !== -1) {
        setFields = formatSetFields(list[setFieldsIndex]);
        list.splice(setFieldsIndex, 1);
    }

    let cond = {};
    let condIndex = getCondIndex(list);
    if (condIndex !== -1) {
        cond = formatCond(list[condIndex]);
        list.splice(condIndex, 1);
    }

    if (showCount) {
        if (_verbose) {
            this.verbose(`db.${table}.count(${j2s(cond)})`);
        }
        return db[table].count(cond, (err, docs)=>{
            if (err) {
                this.print(err, 'red');
            } else {
                showJson(this, docs);
            }
        });
    }

    if (setFields) {
        if (_verbose) {
            this.verbose(`db.${table}.find(${j2s(cond)}).sort(${j2s(sort)}).limit(${limit})`);
        }
        return db[table].find(cond).sort(sort).limit(limit).toArray((err, docs)=>{
            if (err || !docs.length) {
                this.print('no record', 'red');
                this.prompt();
            } else {
                for (const doc of docs) {
                    this.verbose(list[0]);
                    const update = formatUpdate(this, doc, setFields);
                    if (!update) {
                        continue;
                    }
                    if (_verbose) {
                        this.verbose(`db.${table}.update(${doc._id}, ${j2s(update)})`);
                    }
                    db[table].findAndModify({
                        query: { _id: doc._id },
                        update,
                        new: true
                    }, (err, ret) => {
                        if (err) {
                            this.print(err, 'red');
                        } else {
                            showJson(this, ret);
                        }
                    });
                }
            }
        });
    }

    let hasRemove;
    let removeIndex = _.findIndex(list, o=>o==='r');;
    if (removeIndex !== -1) {
        hasRemove = true;
        list.splice(removeIndex, 1);
    }
    if (hasRemove) {
        if (_verbose) {
            this.verbose(`db.${table}.remove(${j2s(cond)}).limit(${limit})`);
        }
        return db[table].find(cond).sort(sort).limit(limit).toArray((err, docs)=>{
            if (err) {
                this.print(err, 'red');
            } else {
                if (!docs.length) {
                    this.prompt();
                }
                for (const doc of docs) {
                    db[table].remove({ _id: doc._id }, true, (err, ret) => {
                        if (err) {
                            this.print(err, 'red');
                        } else {
                            showJson(this, ret);
                        }
                    });
                }
            }
        });
    }

    const fields = await formatFields(db, table, list[0], cond);
    if (_verbose) {
        this.verbose(`db.${table}.find(${j2s(cond)}, ${j2s(fields)}).sort(${j2s(sort)}).limit(${limit})`);
    }
    db[table].find(cond, fields).sort(sort).limit(limit).toArray((err, docs)=>{
        if (err) {
            this.print('no record', 'red');
        } else if (writeFile) {
            fs.writeFileSync(writeFile, `module.exports=${JSON.stringify(formatResult(docs), null, 2)};`);
            this.print('write result to ' + writeFile);
            this.prompt();
        } else {
            showJson(this, docs);
        }
    });
}
async function modifyTable(line, { db }) {
    if (!line) {
        return this.prompt();
    }
    let list = formatLine(this, line);
    const table = list[0];
    if (!table) {
        return this.error('invalid oper');
    }
    list = _.drop(list);

    let limit = 0;
    let limitIndex = getLimitIndex(list);
    if (limitIndex !== -1) {
        limit = +list[limitIndex];
        list.splice(limitIndex, 1);
    } else {
        limit = _limit;
    }

    let sort = '';
    let sortIndex = getSortIndex(list);
    if (sortIndex !== -1) {
        sort = await formatSort(db, table, list[sortIndex]);
        list.splice(sortIndex, 1);
    }

    let setFieldsIndex = getSetFieldsIndex(list);
    if (setFieldsIndex === -1) {
        return this.error('no set fields');
    }
    const setFields = formatSetFields(list[setFieldsIndex]);
    if (!setFields) {
        return this.error('set fields error');
    }
    list.splice(setFieldsIndex, 1);

    let cond = {};
    let condIndex = getCondIndex(list);
    if (condIndex !== -1) {
        cond = formatCond(list[condIndex]);
        list.splice(condIndex, 1);
    }

    if (_verbose) {
        this.verbose(`db.${table}.find(${j2s(cond)}).sort(${j2s(sort)}).limit(${limit})`);
    }
    db[table].find(cond).sort(sort).limit(limit).toArray((err, docs)=>{
        if (err || !docs.length) {
            this.print('no record', 'red');
            this.prompt();
        } else {
            for (const doc of docs) {
                this.verbose(list[0]);
                const update = formatUpdate(this, doc, setFields);
                if (!update) {
                    continue;
                }
                if (_verbose) {
                    this.verbose(`db.${table}.update(${doc._id}, ${j2s(update)})`);
                }
                db[table].findAndModify({
                    query: { _id: doc._id },
                    update,
                    new: true
                }, (err, ret) => {
                    if (err) {
                        this.print(err, 'red');
                    } else {
                        showJson(this, ret);
                    }
                });
            }
        }
    });
}
async function removeTable(line, { db }) {
    if (!line) {
        return this.prompt();
    }
    let list = formatLine(this, line);
    const table = list[0];
    if (!table) {
        return this.error('invalid oper');
    }
    list = _.drop(list);

    let limit = 0;
    let limitIndex = getLimitIndex(list);
    if (limitIndex !== -1) {
        limit = +list[limitIndex];
        list.splice(limitIndex, 1);
    } else {
        limit = _limit;
    }

    let sort = '';
    let sortIndex = getSortIndex(list);
    if (sortIndex !== -1) {
        sort = await formatSort(db, table, list[sortIndex]);
        list.splice(sortIndex, 1);
    }

    let cond = {};
    let condIndex = getCondIndex(list);
    if (condIndex !== -1) {
        cond = formatCond(list[condIndex]);
        list.splice(condIndex, 1);
    }

    if (_verbose) {
        this.verbose(`db.${table}.remove(${j2s(cond)}).limit(${limit})`);
    }
    db[table].find(cond).sort(sort).limit(limit).toArray((err, docs)=>{
        if (err) {
            this.print(err, 'red');
        } else {
            if (!docs.length) {
                this.prompt();
            }
            for (const doc of docs) {
                db[table].remove({ _id: doc._id }, true, (err, ret) => {
                    if (err) {
                        this.print(err, 'red');
                    } else {
                        showJson(this, ret);
                    }
                });
            }
        }
    });
}
async function copyTableRows(line, { db }) {
    if (!line) {
        return this.prompt();
    }
    let list = formatLine(this, line);
    const table = list[0];
    if (!table) {
        return this.error('invalid oper');
    }
    list = _.drop(list);

    let limit = 0;
    let limitIndex = getLimitIndex(list);
    if (limitIndex !== -1) {
        limit = +list[limitIndex];
        list.splice(limitIndex, 1);
    } else {
        limit = _limit;
    }

    let sort = '';
    let sortIndex = getSortIndex(list);
    if (sortIndex !== -1) {
        sort = await formatSort(db, table, list[sortIndex]);
        list.splice(sortIndex, 1);
    }

    let setFieldsIndex = getSetFieldsIndex(list);
    let setFields = null;
    if (setFieldsIndex !== -1) {
        setFields = list[setFieldsIndex];
        setFields = formatSetFields(setFields);
        if (!setFields) {
            return this.error('set fields error');
        }
        list.splice(setFieldsIndex, 1);
    }

    let cond = {};
    let condIndex = getCondIndex(list);
    if (condIndex !== -1) {
        cond = formatCond(list[condIndex]);
        list.splice(condIndex, 1);
    }

    if (_verbose) {
        this.verbose(`db.${table}.copy(${j2s(cond)}).to(${j2s(setFields)}).sort(${j2s(sort)}).limit(${limit})`);
    }
    db[table].find(cond).sort(sort).limit(limit).toArray((err, docs)=>{
        if (err) {
            this.print('no record', 'red');
        } else {
            if (!docs.length) {
                this.prompt();
            }
            for (const doc of docs) {
                const { _id, ..._doc } = doc;
                db[table].save(_doc, (err, ret) => {
                    if (err) {
                        this.print(err, 'red');
                    } else {
                        if (setFields) {
                            const update = formatUpdate(this, _doc, setFields);
                            db[table].findAndModify({
                                query: { _id: ret._id },
                                update,
                                new: true
                            }, (err, ret) => {
                                if (err) {
                                    this.print(err, 'red');
                                } else {
                                    showJson(this, ret);
                                }
                            });
                        } else {
                            showJson(this, ret);
                        }
                    }
                });
            }
        }
    });
}
function togglePrettyShell() {
    _prettyShell = !_prettyShell;
    this.print('pretty format change to ' + _prettyShell);
    this.prompt();
}
function toggleVerbose() {
    _verbose = !_verbose;
    this.print('verbose format change to ' + _verbose);
    this.prompt();
}
function toggleRawMode() {
    _rawMode = !_rawMode;
    this.print('rawMode format change to ' + _rawMode);
    this.prompt();
}
function setHighLightStyle(value) {
    _highLightStyle = value == 0 ? 'railscasts' : value == 1 ? 'github' : _highLightStyle == 'railscasts' ? 'github' : 'railscasts';
    this.print('highLightStyle set to ' + _highLightStyle);
    this.prompt();
}
function setMaxLimit(line) {
    _maxLimit = +line || 1000;
    this.print('set limit to ' + _maxLimit);
    this.prompt();
}
async function changeToDatabase(line, { db }) {
    if (!line) {
        this.prompt();
        return;
    }
    db.close();
    _database = line;
    db = mongojs(`mongodb://${_host}:27017/${_database}`);
    for (const table of CACHE._tables) {
        delete CACHE[table];
    }
    CACHE._options.db = db;
    const tables = await getTables(db, true);
    for (const table of tables) {
        await getFieldsOfTable(db, table, true);
    }
    fs.writeJSONSync(configFile, { host: _host, database: _database });
    this.prompt();
}
async function dumpDatabase(line = '') {
    let list = line.split(/\s+/).filter(Boolean);
    let index = list.indexOf('-o');
    let dir;
    if (index !== -1) {
        const items = list.splice(index, 2);
        dir = items[1];
    }
    if (!dir) {
        dir = _database;
    }
    if (fs.existsSync(path.join(dir, _database))) {
        return this.error(`${path.join(dir, _database)} 已经存在`);
    }
    let condition;
    if (_.find(list, o=>/^-/.test(o))) {
        condition = _.map(list, o=>`--excludeCollection ${o.replace(/^-/, '')}`).join(' ');
    } else {
        condition = _.map(list, o=>`--collection ${o}`).join(' ');
    }

    const cmd = `mongodump -h ${_host} -d ${_database} ${condition} -o ${dir}`;
    this.log(`cmd: ${cmd}`);
    shell.exec(cmd, { silent:true }, (code, stdout, stderr) => {
        if (stderr && /error/.test(stderr)) {
            return this.error(stderr);
        }
        this.success(`目标文件：${process.cwd()}/${dir}`);
    });
}
async function restoreDatabase(line = '', { db }) {
    const dbname = line.replace(/.*\//, '');
    const cmd = `mongorestore -d ${dbname} ${line}`;
    this.log(cmd);
    shell.exec(cmd, { silent:true }, (code, stdout, stderr) => {
        if (stderr && /error/.test(stderr)) {
            return this.error(stderr);
        }
        this.success(`成功导入 ${dbname}`);
        changeToDatabase.call(this, dbname, { db });
    });
}
async function moveDatabase(line = '', { db }) {
    let list = line.split(/\s+/).filter(Boolean);
    if (list.length < 2) {
        return this.error('格式不正确');
    }
    await nativeMongo(this, `
        var source = '${list[0]}';
        var dest = '${list[1]}';
        var colls = db.getSiblingDB(source).getCollectionNames();
        for (var i = 0; i < colls.length; i++) {
            var from = source +  '.' + colls[i];
            var to = dest + '.' + colls[i];
            db.adminCommand({renameCollection: from, to: to});
        }`, true
    );
    changeToDatabase.call(this, list[1], { db });
}
function showHelp (isFull) {
    this.print('commands:', 'blue');
    this.print('    h: show help');
    this.print('    help: show full help');
    this.print('    <q|exit>: exit');
    this.print('    cd: change to database');
    this.print('    ls: showTables|ls ..: showDatabases|ls table: showTableFields');
    this.print('    s(showTableRows): s table f1,f2>,<f3> (f1=xx&f2=yy)|f3=zz|f4~<xx>|f5#1,2,3 limit(-1:count) sortField[+-] [out=file] [r] [f1:xx]');
    this.print('    m(modifyTable): m table f1:xx;f2:parseFunction $cond limit sortField[+-]');
    this.print('    r(removeTable): # r table $cond limit sortField[+-]');
    this.print('    c(copyTableRows): c table $update $cond limit sortField[+-]');
    isFull && this.print('    mv(moveDatabase): move dbname1 dbname2');
    isFull && this.print('    dump(dumpDatabase): dump [[-]table1 table2 [-o outdir]]');
    isFull && this.print('    restore(restoreDatabase): restore path/db')
    isFull && this.print('    format: [undefined: $unset][id: \"xx\"|\'xx\':string xx:ObjectId][date:\"xx\":string xx|\'xx\':date][number:\"xx\"|\'xx\':string xx:number]');
    isFull && this.print('    parseFunction:=>xx|return xx|($, {_, moment, utils, $$})=>xx|@xx.js|=x$x');
    isFull && this.print('    _: togglePrettyShell');
    isFull && this.print('    v: toggleVerbose');
    isFull && this.print('    raw: toggleRawMode');
    isFull && this.print('    hl: setHighLightStyle 0: railscasts 1: github else toggle');
    isFull && this.print('    limit: setMaxLimit');
    this.print('    native: db.xx.update({},{\'$set\':\'xx.yy\':1})');
    this.print('');
    this.prompt();
}
function showFullHelp() {
    showHelp.call(this, true);
}
const COMMANDS = {
    'h': showHelp,
    'help': showFullHelp,
    'q|exit': function () { this.close() },
    'cls': function() { this.clear() },
    'cd': changeToDatabase,
    'ls': showTables,
    's': showTableRows,
    'm': modifyTable,
    'r': removeTable,
    'c': copyTableRows,
    'mv': moveDatabase,
    'dump': dumpDatabase,
    'restore': restoreDatabase,
    '_': togglePrettyShell,
    'v': toggleVerbose,
    'raw': toggleRawMode,
    'hl': setHighLightStyle,
    'limit': setMaxLimit,
    'default': executeLine,
};


program
.version('1.0.0')
.option('-t, --host [host]', 'set host', '')
.option('-d, --database [database]', 'set database', '')
.option('-c, --config', 'show config', '')
.parse(process.argv);

let options = {};
try {
    options = fs.readJSONSync(configFile);
} catch (e) {}

_host = program.host || options.host || _host;
_database = program.database || options.database || _database;
if (options.host !== _host || options.database !== _database) {
    fs.writeJSONSync(configFile, { host: _host, database: _database });
}
if (program.config) {
    console.log({ host: _host, database: _database });
    process.exit(0);
}
createMongo(COMMANDS, { title: 'mongo', history: [ 's', 'm', 'r', 'c', 'dump', 'restore', 'export' ] });
